// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.util.*;
import java.io.*;
import java.net.*;

/**
 * The Lookup object issues queries to caching DNS servers. The input consists
 * of a name, an optional type, and an optional class. Caching is enabled by
 * default and used when possible to reduce the number of DNS requests. A
 * Resolver, which defaults to an ExtendedResolver initialized with the
 * resolvers located by the ResolverConfig class, performs the queries. A search
 * path of domain suffixes is used to resolve relative names, and is also
 * determined by the ResolverConfig class.
 * 
 * A Lookup object may be reused, but should not be used by multiple threads.
 * 
 * @see Cache
 * @see Resolver
 * @see ResolverConfig
 * 
 * @author Brian Wellington
 */

public final class Lookup {

	private static Resolver defaultResolver;
	private static Name[] defaultSearchPath;
	private static Map defaultCaches;
	private static int defaultNdots;

	private Resolver resolver;
	private Name[] searchPath;
	private Cache cache;
	private boolean temporary_cache;
	private int credibility;
	private Name name;
	private int type;
	private int dclass;
	private boolean verbose;
	private int iterations;
	private boolean foundAlias;
	private boolean done;
	private boolean doneCurrent;
	private List aliases;
	private Record[] answers;
	private int result;
	private String error;
	private boolean nxdomain;
	private boolean badresponse;
	private String badresponse_error;
	private boolean networkerror;
	private boolean timedout;
	private boolean nametoolong;
	private boolean referral;

	private static final Name[] noAliases = new Name[0];

	/** The lookup was successful. */
	public static final int SUCCESSFUL = 0;

	/**
	 * The lookup failed due to a data or server error. Repeating the lookup
	 * would not be helpful.
	 */
	public static final int UNRECOVERABLE = 1;

	/**
	 * The lookup failed due to a network error. Repeating the lookup may be
	 * helpful.
	 */
	public static final int TRY_AGAIN = 2;

	/** The host does not exist. */
	public static final int HOST_NOT_FOUND = 3;

	/** The host exists, but has no records associated with the queried type. */
	public static final int TYPE_NOT_FOUND = 4;

	public static synchronized void refreshDefault() {

		try {
			defaultResolver = new ExtendedResolver();
		} catch (UnknownHostException e) {
			throw new RuntimeException("Failed to initialize resolver");
		}
		defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath();
		defaultCaches = new HashMap();
		defaultNdots = ResolverConfig.getCurrentConfig().ndots();
	}

	static {
		refreshDefault();
	}

	/**
	 * Gets the Resolver that will be used as the default by future Lookups.
	 * 
	 * @return The default resolver.
	 */
	public static synchronized Resolver getDefaultResolver() {
		return defaultResolver;
	}

	/**
	 * Sets the default Resolver to be used as the default by future Lookups.
	 * 
	 * @param resolver
	 *            The default resolver.
	 */
	public static synchronized void setDefaultResolver(Resolver resolver) {
		defaultResolver = resolver;
	}

	/**
	 * Gets the Cache that will be used as the default for the specified class
	 * by future Lookups.
	 * 
	 * @param dclass
	 *            The class whose cache is being retrieved.
	 * @return The default cache for the specified class.
	 */
	public static synchronized Cache getDefaultCache(int dclass) {
		DClass.check(dclass);
		Cache c = (Cache) defaultCaches.get(Mnemonic.toInteger(dclass));
		if (c == null) {
			c = new Cache(dclass);
			defaultCaches.put(Mnemonic.toInteger(dclass), c);
		}
		return c;
	}

	/**
	 * Sets the Cache to be used as the default for the specified class by
	 * future Lookups.
	 * 
	 * @param cache
	 *            The default cache for the specified class.
	 * @param dclass
	 *            The class whose cache is being set.
	 */
	public static synchronized void setDefaultCache(Cache cache, int dclass) {
		DClass.check(dclass);
		defaultCaches.put(Mnemonic.toInteger(dclass), cache);
	}

	/**
	 * Gets the search path that will be used as the default by future Lookups.
	 * 
	 * @return The default search path.
	 */
	public static synchronized Name[] getDefaultSearchPath() {
		return defaultSearchPath;
	}

	/**
	 * Sets the search path to be used as the default by future Lookups.
	 * 
	 * @param domains
	 *            The default search path.
	 */
	public static synchronized void setDefaultSearchPath(Name[] domains) {
		defaultSearchPath = domains;
	}

	/**
	 * Sets the search path that will be used as the default by future Lookups.
	 * 
	 * @param domains
	 *            The default search path.
	 * @throws TextParseException
	 *             A name in the array is not a valid DNS name.
	 */
	public static synchronized void setDefaultSearchPath(String[] domains)
			throws TextParseException {
		if (domains == null) {
			defaultSearchPath = null;
			return;
		}
		Name[] newdomains = new Name[domains.length];
		for (int i = 0; i < domains.length; i++)
			newdomains[i] = Name.fromString(domains[i], Name.root);
		defaultSearchPath = newdomains;
	}

	private final void reset() {
		iterations = 0;
		foundAlias = false;
		done = false;
		doneCurrent = false;
		aliases = null;
		answers = null;
		result = -1;
		error = null;
		nxdomain = false;
		badresponse = false;
		badresponse_error = null;
		networkerror = false;
		timedout = false;
		nametoolong = false;
		referral = false;
		if (temporary_cache)
			cache.clearCache();
	}

	/**
	 * Create a Lookup object that will find records of the given name, type,
	 * and class. The lookup will use the default cache, resolver, and search
	 * path, and look for records that are reasonably credible.
	 * 
	 * @param name
	 *            The name of the desired records
	 * @param type
	 *            The type of the desired records
	 * @param dclass
	 *            The class of the desired records
	 * @throws IllegalArgumentException
	 *             The type is a meta type other than ANY.
	 * @see Cache
	 * @see Resolver
	 * @see Credibility
	 * @see Name
	 * @see Type
	 * @see DClass
	 */
	public Lookup(Name name, int type, int dclass) {
		Type.check(type);
		DClass.check(dclass);
		if (!Type.isRR(type) && type != Type.ANY)
			throw new IllegalArgumentException("Cannot query for "
					+ "meta-types other than ANY");
		this.name = name;
		this.type = type;
		this.dclass = dclass;
		synchronized (Lookup.class) {
			this.resolver = getDefaultResolver();
			this.searchPath = getDefaultSearchPath();
			this.cache = getDefaultCache(dclass);
		}
		this.credibility = Credibility.NORMAL;
		this.verbose = Options.check("verbose");
		this.result = -1;
	}

	/**
	 * Create a Lookup object that will find records of the given name and type
	 * in the IN class.
	 * 
	 * @param name
	 *            The name of the desired records
	 * @param type
	 *            The type of the desired records
	 * @throws IllegalArgumentException
	 *             The type is a meta type other than ANY.
	 * @see #Lookup(Name,int,int)
	 */
	public Lookup(Name name, int type) {
		this(name, type, DClass.IN);
	}

	/**
	 * Create a Lookup object that will find records of type A at the given name
	 * in the IN class.
	 * 
	 * @param name
	 *            The name of the desired records
	 * @see #Lookup(Name,int,int)
	 */
	public Lookup(Name name) {
		this(name, Type.A, DClass.IN);
	}

	/**
	 * Create a Lookup object that will find records of the given name, type,
	 * and class.
	 * 
	 * @param name
	 *            The name of the desired records
	 * @param type
	 *            The type of the desired records
	 * @param dclass
	 *            The class of the desired records
	 * @throws TextParseException
	 *             The name is not a valid DNS name
	 * @throws IllegalArgumentException
	 *             The type is a meta type other than ANY.
	 * @see #Lookup(Name,int,int)
	 */
	public Lookup(String name, int type, int dclass) throws TextParseException {
		this(Name.fromString(name), type, dclass);
	}

	/**
	 * Create a Lookup object that will find records of the given name and type
	 * in the IN class.
	 * 
	 * @param name
	 *            The name of the desired records
	 * @param type
	 *            The type of the desired records
	 * @throws TextParseException
	 *             The name is not a valid DNS name
	 * @throws IllegalArgumentException
	 *             The type is a meta type other than ANY.
	 * @see #Lookup(Name,int,int)
	 */
	public Lookup(String name, int type) throws TextParseException {
		this(Name.fromString(name), type, DClass.IN);
	}

	/**
	 * Create a Lookup object that will find records of type A at the given name
	 * in the IN class.
	 * 
	 * @param name
	 *            The name of the desired records
	 * @throws TextParseException
	 *             The name is not a valid DNS name
	 * @see #Lookup(Name,int,int)
	 */
	public Lookup(String name) throws TextParseException {
		this(Name.fromString(name), Type.A, DClass.IN);
	}

	/**
	 * Sets the resolver to use when performing this lookup. This overrides the
	 * default value.
	 * 
	 * @param resolver
	 *            The resolver to use.
	 */
	public void setResolver(Resolver resolver) {
		this.resolver = resolver;
	}

	/**
	 * Sets the search path to use when performing this lookup. This overrides
	 * the default value.
	 * 
	 * @param domains
	 *            An array of names containing the search path.
	 */
	public void setSearchPath(Name[] domains) {
		this.searchPath = domains;
	}

	/**
	 * Sets the search path to use when performing this lookup. This overrides
	 * the default value.
	 * 
	 * @param domains
	 *            An array of names containing the search path.
	 * @throws TextParseException
	 *             A name in the array is not a valid DNS name.
	 */
	public void setSearchPath(String[] domains) throws TextParseException {
		if (domains == null) {
			this.searchPath = null;
			return;
		}
		Name[] newdomains = new Name[domains.length];
		for (int i = 0; i < domains.length; i++)
			newdomains[i] = Name.fromString(domains[i], Name.root);
		this.searchPath = newdomains;
	}

	/**
	 * Sets the cache to use when performing this lookup. This overrides the
	 * default value. If the results of this lookup should not be permanently
	 * cached, null can be provided here.
	 * 
	 * @param cache
	 *            The cache to use.
	 */
	public void setCache(Cache cache) {
		if (cache == null) {
			this.cache = new Cache(dclass);
			this.temporary_cache = true;
		} else {
			this.cache = cache;
			this.temporary_cache = false;
		}
	}

	/**
	 * Sets ndots to use when performing this lookup, overriding the default
	 * value. Specifically, this refers to the number of "dots" which, if
	 * present in a name, indicate that a lookup for the absolute name should be
	 * attempted before appending any search path elements.
	 * 
	 * @param ndots
	 *            The ndots value to use, which must be greater than or equal to
	 *            0.
	 */
	public void setNdots(int ndots) {
		if (ndots < 0)
			throw new IllegalArgumentException("Illegal ndots value: " + ndots);
		defaultNdots = ndots;
	}

	/**
	 * Sets the minimum credibility level that will be accepted when performing
	 * the lookup. This defaults to Credibility.NORMAL.
	 * 
	 * @param credibility
	 *            The minimum credibility level.
	 */
	public void setCredibility(int credibility) {
		this.credibility = credibility;
	}

	private void follow(Name name, Name oldname) {
		foundAlias = true;
		badresponse = false;
		networkerror = false;
		timedout = false;
		nxdomain = false;
		referral = false;
		iterations++;
		if (iterations >= 6 || name.equals(oldname)) {
			result = UNRECOVERABLE;
			error = "CNAME loop";
			done = true;
			return;
		}
		if (aliases == null)
			aliases = new ArrayList();
		aliases.add(oldname);
		lookup(name);
	}

	private void processResponse(Name name, SetResponse response) {
		if (response.isSuccessful()) {
			RRset[] rrsets = response.answers();
			List l = new ArrayList();
			Iterator it;
			int i;

			for (i = 0; i < rrsets.length; i++) {
				it = rrsets[i].rrs();
				while (it.hasNext())
					l.add(it.next());
			}

			result = SUCCESSFUL;
			answers = (Record[]) l.toArray(new Record[l.size()]);
			done = true;
		} else if (response.isNXDOMAIN()) {
			nxdomain = true;
			doneCurrent = true;
			if (iterations > 0) {
				result = HOST_NOT_FOUND;
				done = true;
			}
		} else if (response.isNXRRSET()) {
			result = TYPE_NOT_FOUND;
			answers = null;
			done = true;
		} else if (response.isCNAME()) {
			CNAMERecord cname = response.getCNAME();
			follow(cname.getTarget(), name);
		} else if (response.isDNAME()) {
			DNAMERecord dname = response.getDNAME();
			try {
				follow(name.fromDNAME(dname), name);
			} catch (NameTooLongException e) {
				result = UNRECOVERABLE;
				error = "Invalid DNAME target";
				done = true;
			}
		} else if (response.isDelegation()) {
			// We shouldn't get a referral. Ignore it.
			referral = true;
		}
	}

	private void lookup(Name current) {
		SetResponse sr = cache.lookupRecords(current, type, credibility);
		if (verbose) {
			System.err.println("lookup " + current + " " + Type.string(type));
			System.err.println(sr);
		}
		processResponse(current, sr);
		if (done || doneCurrent)
			return;

		Record question = Record.newRecord(current, type, dclass);
		Message query = Message.newQuery(question);
		Message response = null;
		try {
			response = resolver.send(query);
		} catch (IOException e) {
			// A network error occurred. Press on.
			if (e instanceof InterruptedIOException)
				timedout = true;
			else
				networkerror = true;
			return;
		}
		int rcode = response.getHeader().getRcode();
		if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) {
			// The server we contacted is broken or otherwise unhelpful.
			// Press on.
			badresponse = true;
			badresponse_error = Rcode.string(rcode);
			return;
		}

		if (!query.getQuestion().equals(response.getQuestion())) {
			// The answer doesn't match the question. That's not good.
			badresponse = true;
			badresponse_error = "response does not match query";
			return;
		}

		sr = cache.addMessage(response);
		if (sr == null)
			sr = cache.lookupRecords(current, type, credibility);
		if (verbose) {
			System.err.println("queried " + current + " " + Type.string(type));
			System.err.println(sr);
		}
		processResponse(current, sr);
	}

	private void resolve(Name current, Name suffix) {
		doneCurrent = false;
		Name tname = null;
		if (suffix == null)
			tname = current;
		else {
			try {
				tname = Name.concatenate(current, suffix);
			} catch (NameTooLongException e) {
				nametoolong = true;
				return;
			}
		}
		lookup(tname);
	}

	/**
	 * Performs the lookup, using the specified Cache, Resolver, and search
	 * path.
	 * 
	 * @return The answers, or null if none are found.
	 */
	public Record[] run() {
		if (done)
			reset();
		if (name.isAbsolute())
			resolve(name, null);
		else if (searchPath == null)
			resolve(name, Name.root);
		else {
			if (name.labels() > defaultNdots)
				resolve(name, Name.root);
			if (done)
				return answers;

			for (int i = 0; i < searchPath.length; i++) {
				resolve(name, searchPath[i]);
				if (done)
					return answers;
				else if (foundAlias)
					break;
			}
		}
		if (!done) {
			if (badresponse) {
				result = TRY_AGAIN;
				error = badresponse_error;
				done = true;
			} else if (timedout) {
				result = TRY_AGAIN;
				error = "timed out";
				done = true;
			} else if (networkerror) {
				result = TRY_AGAIN;
				error = "network error";
				done = true;
			} else if (nxdomain) {
				result = HOST_NOT_FOUND;
				done = true;
			} else if (referral) {
				result = UNRECOVERABLE;
				error = "referral";
				done = true;
			} else if (nametoolong) {
				result = UNRECOVERABLE;
				error = "name too long";
				done = true;
			}
		}
		return answers;
	}

	private void checkDone() {
		if (done && result != -1)
			return;
		StringBuffer sb = new StringBuffer("Lookup of " + name + " ");
		if (dclass != DClass.IN)
			sb.append(DClass.string(dclass) + " ");
		sb.append(Type.string(type) + " isn't done");
		throw new IllegalStateException(sb.toString());
	}

	/**
	 * Returns the answers from the lookup.
	 * 
	 * @return The answers, or null if none are found.
	 * @throws IllegalStateException
	 *             The lookup has not completed.
	 */
	public Record[] getAnswers() {
		checkDone();
		return answers;
	}

	/**
	 * Returns all known aliases for this name. Whenever a CNAME/DNAME is
	 * followed, an alias is added to this array. The last element in this array
	 * will be the owner name for records in the answer, if there are any.
	 * 
	 * @return The aliases.
	 * @throws IllegalStateException
	 *             The lookup has not completed.
	 */
	public Name[] getAliases() {
		checkDone();
		if (aliases == null)
			return noAliases;
		return (Name[]) aliases.toArray(new Name[aliases.size()]);
	}

	/**
	 * Returns the result code of the lookup.
	 * 
	 * @return The result code, which can be SUCCESSFUL, UNRECOVERABLE,
	 *         TRY_AGAIN, HOST_NOT_FOUND, or TYPE_NOT_FOUND.
	 * @throws IllegalStateException
	 *             The lookup has not completed.
	 */
	public int getResult() {
		checkDone();
		return result;
	}

	/**
	 * Returns an error string describing the result code of this lookup.
	 * 
	 * @return A string, which may either directly correspond the result code or
	 *         be more specific.
	 * @throws IllegalStateException
	 *             The lookup has not completed.
	 */
	public String getErrorString() {
		checkDone();
		if (error != null)
			return error;
		switch (result) {
		case SUCCESSFUL:
			return "successful";
		case UNRECOVERABLE:
			return "unrecoverable error";
		case TRY_AGAIN:
			return "try again";
		case HOST_NOT_FOUND:
			return "host not found";
		case TYPE_NOT_FOUND:
			return "type not found";
		}
		throw new IllegalStateException("unknown result");
	}

}
